跳到主要内容

Go 的 errors 包

errors 包的 文档

这个 errors 包是一个官方整理的异常处理包,它提供了一些非常有用的操作用于封装和处理错误。

本质上在 builtin 类型中,error 被定义为一个 interface,这个类型只包含一个 Error 方法,返回字符串形式的错误内容。

常见的 errors.New 创建一个 error 对象,或通过 error.Error 方法获取 error 中的文本内容

简单使用

package main

import (
"errors"
"fmt"
)

func Oops() error {
return errors.New("iam an error")
}

func Print() {
err := Oops()
fmt.Println("oops, we go an error,", err.Error())
}

实现 error 接口

go 允许函数具有多返回值,而标准库内置的 errorString 类型由于只能表达字符串错误信息显然受限。所以,可以通过实现 error 接口的方式,来扩展错误返回

// 自定义error类型
type EasyError struct {
Msg string // 错误文本信息
Code int64 // 错误码
}

func (me *EasyError) Error() string {
return fmt.Sprintf("code %d, msg %s", me.Code, me.Msg)
}

// Easy 实现了 error 接口,所以可以在 Oops 中返回
func DoSomething() error {
return &EasyError{"easy error", 1}
}

// 业务应用
func DoBusiness() {
err := DoSomething()
e, ok := err.(EasyError)
if ok {
fmt.Printf("code %d, msg %s\n", e.Code, e.Msg)
}
}

现在在自定义的错误类型中塞入了错误码信息。随着业务代码调用层层深入,当最内层的操作(比如数据库操作)发生错误时,我们希望能在业务调用链上每一层都携带错误信息,就像递归调用一样,这时可以用到标准库的 Unwrap 方法

Unwrap 嵌套错误

一旦自定义 error 实现类型定义了 Unwrap 方法,那么它就具有了嵌套的能力,其函数原型定义如下:

// 标准库 Unwrap 方法,传入一个 error 对象,返回其内嵌的 error
func Unwrap(err error) error

虽然 error 接口没有定义 Unwrap 方法,但是标准库的 Unwrap 方法中会通过反射隐式调用自定义类型的 Unwrap 方法,这也是业务实现自定义嵌套的途径。

如下,只需简单的实现这个接口就行了

type EasyError struct {}

/*...*/

// 自定义 Unwrap 方法
func (me *EasyError) Unwrap() error {
// ...
}

使用例:

我们给 EasyError 增加一个 error 成员,表示包含的下一级 error

// 
type EasyError struct {
Msg string // 错误文字信息
Code int64 // 错误码
Nest error // 嵌套的错误
}

func (me *EasyError) Unwrap() error {
return me.Nest
}

func DoSomething1() error {
// ...
err := DoSomething2()
if err != nil {
return &EasyError{"from DoSomething1", 1, err}
}

return nil
}

func DoSomething2() error {
// ...
err := DoSomething3()
if err != nil {
return &EasyError{"from DoSomething2", 2, err}
}

return nil
}

func DoSomething3() error {
// ...
return &EasyError{"from DoSomething3", 3, nil}
}

这样就可以使用 errors.Unwrap 方法去通过嵌套的方式,将调用路径中的错误信息,携带至调用栈的栈底。

递归测试:

func main() {
err := DoSomething1()
for err != nil {
e := err.(*EasyError)
fmt.Printf("code %d, msg %s\n", e.Code, e.Msg)
// errors.Unwrap 中调用 EasyError 的 Unwrap 返回子 error
err = errors.Unwrap(err)
}
}

输出:

// 可以很清楚的看到调用链上产生的错误信息
// Output:
// code 1, msg from DoSomething1
// code 2, msg from DoSomething2
// code 3, msg from DoSomething3

Is 判断两个 error 是否相等

代码中经常会出现 err == nil 或者 err == ErrNotExist 之类的判断,对于 error 类型,由于其是 interface 类型,实际比较的是 interface 接口对象实体的地址。

也就是说,重复的 new 两个文本内容一样的 error 对象,这两个对象并不相等,因为 比较的是这两个对象的地址。这是完全不同的两个对象

// 展示了error比较代码
if errors.New("hello error") == errors.New("hello error") { // false
}

errhello := errors.New("hello error")
if errhello == errhello { // true
}

所以需要使用 Is 方法来判别错误类型,使用例:

type CustomError struct {
Msg string // 错误文字信息
Code int64 // 错误码
Nest error // 嵌套的错误
}

func (e CustomError) Error() string {
return fmt.Sprintf("code %d, msg %s\n", e.Code, e.Msg)
}

func (e CustomError) Is(target error) bool {
_, ok := target.(*CustomError)
return ok
}

func main() {
err01 := errors.New("this is error")
err02 := &CustomError{"this is custom error", 404, nil}
err03 := &CustomError{"this is custom error2", 404, nil}

is := errors.Is(err02, err01)
fmt.Println(is)

is2 := errors.Is(err02, err03)
fmt.Println(is2)
}

打印:

false
true

这个 errors.Is 内部会通过反射来调用上面用户自定义的 Is 方法

可以看到它会尝试调用 Unwrap 方法递归地查找错误

As 方法与错误信息读取

现在通过 Is 实现了分类,可以判断一个错误是否是某个类型,但是更进一步,如果我们想得到不同错误类型的详细信息呢?业务层拿到返回的 error,就不得不通过层层 Unwrap 和类型断言来获取调用链中的深层错误信息。

所以 errors 包提供了 As 方法,在 Unwrap 的基础上,直接获取 error 接口中,实际是 error 链中指定类型的错误。

所以它的用途就是断言

func test() {
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
}

如上代码,通过 As 来断言 err 是否为 PathError 类型的错误,实际上内部和上面 Is 方法是一样的

注意!!这里 As 传入的第二个参数是指针的指针,如下示例:

var _ error = (*CustomError)(nil) // ensure CustomError implements error

type CustomError struct {
msg string
}

func (e CustomError) Error() string {
return e.msg
}

func main() {
err := &CustomError{"Hello, world!"} // Methods return pointers to errors, allowing them to be nil

var eval *CustomError

as := errors.As(err, &eval) // yes, that's **CustomError
asFaulty := errors.As(err, eval) // no compile error, so it wrongly seems okay
is := errors.Is(err, eval) // that's just *CustomError

fmt.Printf("as: %t, asFaulty: %t, is: %t", as, asFaulty, is) // as: true, asFaulty: false, is: true
}

Reference

Golang标准库:errors包应用 Error wrap/unwrap && type checking with errors.Is() errors 包的 文档